目录
  1. 1. 一、Android 中的 Linux 系统调用
  2. 2. 二、文件 I/O:系统调用 vs C 库
    1. 2.1. 2.1 无缓冲 I/O(系统调用)
    2. 2.2. 2.2 缓冲 I/O(C 标准库)
    3. 2.3. 2.3 实用技巧:mmap 文件映射
  3. 3. 三、进程管理:Zygote 模型
  4. 4. 四、信号处理
  5. 5. 五、epoll:高效 I/O 多路复用
  6. 6. 六、Unix Domain Socket
  7. 7. 七、匿名共享内存(ashmem)
  8. 8. 八、CMake 构建配置
  9. 9. 九、面试常问题目
【C/C++理论实战技术】Linux编程实战

一、Android 中的 Linux 系统调用

Android 基于 Linux 内核,所有底层操作最终都通过系统调用(syscall)完成。NDK 开发中可以直接使用 POSIX API,这些 API 内部封装了系统调用:

类别 系统调用 函数封装 AOSP 使用场景
文件 I/O open/read/write/close fopen/fread/fwrite/fclose 文件读写
进程管理 fork/execve/waitpid fork/exec/wait Zygote 进程孵化
内存管理 mmap/munmap/brk malloc/free(内部调用) Binder 内存映射、匿名共享内存
线程同步 futex/clone pthread_create/pthread_mutex_lock 所有线程操作
网络 I/O socket/bind/connect/send/recv getaddrinfo/connect 网络通信
设备控制 ioctl OS 封装函数 HAL 层与硬件通信
文件监控 inotify_add_watch FileObserver(Java API) 文件变化监听

Android 的系统调用表在 bionic/libc/kernel/uapi/asm-generic/unistd.h 中定义。32 位 ARM 架构使用 __NR_ 前缀(如 __NR_openat),通过 svc #0 指令触发;64 位 ARM 使用 svc #0 但系统调用号表不同。

二、文件 I/O:系统调用 vs C 库

Android NDK 中,文件 I/O 有两种方式:

2.1 无缓冲 I/O(系统调用)

#include <fcntl.h>
#include <unistd.h>

int fd = open("/sdcard/test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open failed");
return;
}

const char *data = "Hello from native code\n";
ssize_t written = write(fd, data, strlen(data));
if (written < 0) {
perror("write failed");
}

// 移动文件指针
lseek(fd, 0, SEEK_SET);

char buffer[128];
ssize_t nread = read(fd, buffer, sizeof(buffer) - 1);
if (nread > 0) {
buffer[nread] = '\0';
}

close(fd);

openreadwriteclose 是系统调用,每次调用都涉及用户态到内核态的切换,开销较大。

2.2 缓冲 I/O(C 标准库)

#include <stdio.h>

FILE *fp = fopen("/sdcard/test.txt", "w+");
if (fp == NULL) {
perror("fopen failed");
return;
}

fprintf(fp, "Line %d\n", 1);
fputs("Hello from native\n", fp);

// 将缓冲区内容刷到磁盘
fflush(fp);

rewind(fp);

char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
printf("Read: %s", line);
}

fclose(fp);

FILE* 系列函数在用户态维护了一个缓冲区(默认 8KB),减少了系统调用的次数。但当需要精确的控制(如实时性要求高的场景、需要对文件描述符进行 poll/epoll/select 操作)时,应该使用无缓冲 I/O。

2.3 实用技巧:mmap 文件映射

#include <sys/mman.h>
#include <sys/stat.h>

int fd = open("/sdcard/large_file.bin", O_RDONLY);
struct stat st;
fstat(fd, &st);

// 将文件映射到虚拟地址空间
void *mapped = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mapped == MAP_FAILED) {
perror("mmap failed");
close(fd);
return;
}

// 直接通过内存访问文件内容(无需 read/write 系统调用)
// 操作系统按需从磁盘加载页面(demand paging)
process_data((uint8_t *)mapped, st.st_size);

// 解除映射
munmap(mapped, st.st_size);
close(fd);

Android 的 ART 运行时加载 DEX/OAT 文件时使用 mmap 将其直接映射到进程空间,从而实现高效的代码和数据访问。Binder 驱动的数据传输也基于 mmap(一次性映射 1MB-16KB 的内核缓冲区,后续数据传输无需额外拷贝)。

源码路径:frameworks/native/libs/binder/ProcessState.cppmmap(NULL, BINDER_VM_SIZE, ...)

三、进程管理:Zygote 模型

Android 应用进程不是通过传统 fork+exec 创建的,而是通过 Zygote 进程fork 机制孵化,这极大地加速了应用启动。

init 进程
→ Zygote 进程 (app_process / system/bin/app_process64)
预加载 Framework 类、JNI 库、通用资源
→ fork() → 应用进程 1 (com.example.app1)
→ fork() → 应用进程 2 (com.example.app2)
→ fork() → 应用进程 3 (com.example.app3)

Zygote 的精妙之处在于 Linux fork 的 Copy-on-Write(CoW)特性:

  • fork() 后,子进程与父进程共享物理内存页(只读)。
  • 只有当子进程尝试写入某一页时,内核才会为该页创建一份副本。
  • 因此,Zygote 预加载的 Framework 代码和资源在所有应用进程之间共享,大大减少了整体内存占用。

在 NDK 中,fork() 可以直接使用,但 Android 对其有限制:

#include <unistd.h>
#include <sys/wait.h>

pid_t pid = fork();
if (pid == 0) {
// 子进程
// 注意:子进程只有一个线程(fork 调用者),其他线程不复制
// 子进程中不能安全使用大多数 Android API
execl("/system/bin/logcat", "logcat", "-d", NULL);
_exit(0); // 子进程使用 _exit 而非 exit
} else if (pid > 0) {
// 父进程
int status;
waitpid(pid, &status, 0); // 等待子进程结束
if (WIFEXITED(status)) {
printf("Child exited with %d\n", WEXITSTATUS(status));
}
} else {
perror("fork failed");
}

fork() 后子进程中的注意事项:

  1. 只有调用 fork() 的线程被复制到子进程。其它线程消失(可能导致持有的锁无法释放,造成死锁)。
  2. 子进程必须使用 _exit() 而不是 exit(),因为 exit() 会调用 atexit 注册的清理函数。
  3. 子进程不能安全地使用父进程的 JNIEnv(需要通过 AttachCurrentThread 重新获取)。

四、信号处理

信号(Signal)是 Linux 中进程间异步通知的机制。Android 的 tombstone(崩溃日志)就是由 debuggerd 捕获 crash 信号后生成的。

#include <signal.h>
#include <string.h>

void crash_handler(int sig, siginfo_t *info, void *ucontext) {
// 不要在信号处理器中做复杂操作(如 malloc、printf)
// 只记录关键信息,然后调用 _exit 或重新抛出信号

const char *sig_name = "UNKNOWN";
switch (sig) {
case SIGSEGV: sig_name = "SIGSEGV"; break;
case SIGABRT: sig_name = "SIGABRT"; break;
case SIGFPE: sig_name = "SIGFPE"; break;
case SIGILL: sig_name = "SIGILL"; break;
}

// 使用 write 系统调用输出(信号安全)
write(STDERR_FILENO, "Caught signal: ", 15);
write(STDERR_FILENO, sig_name, strlen(sig_name));
write(STDERR_FILENO, "\n", 1);

// 恢复默认信号处理器并重新触发(让系统生成 tombstone)
signal(sig, SIG_DFL);
raise(sig);
}

void install_crash_handler() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = crash_handler;
sa.sa_flags = SA_SIGINFO | SA_ONSTACK; // SA_ONSTACK 使用备用栈

sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGABRT, &sa, NULL);
sigaction(SIGFPE, &sa, NULL);
}

Android 的 debuggerd 守护进程(源码路径:system/core/debuggerd/)通过 ptrace attach 到 crash 的进程,读取寄存器、调用栈、内存映射等信息,生成 tombstone 文件(保存在 /data/tombstones/)。NDK 开发中分析 native crash 的首要工具就是 tombstone 和 ndk-stack。

信号处理器的限制:

  • 只能使用异步信号安全的函数(man 7 signal-safety)。
  • 不能调用 malloc/freeprintf/fprintfpthread_mutex_lock 等。
  • 安全的函数主要是:writereadopenclose_exitsignalraise
  • 推荐使用 SA_ONSTACK 标志,为信号处理器分配独立栈空间(避免栈溢出时信号处理器无法执行)。

五、epoll:高效 I/O 多路复用

epoll 是 Linux 特有的 I/O 多路复用机制,Android 的 MessageQueue 和网络框架底层都依赖它(Java 层的 Looper → Native 层的 Looper::pollInnerepoll_wait):

#include <sys/epoll.h>

#define MAX_EVENTS 64

int epfd = epoll_create1(EPOLL_CLOEXEC); // 创建 epoll 实例
if (epfd < 0) {
perror("epoll_create1");
return;
}

struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 读事件 + 边缘触发
ev.data.fd = server_fd;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev) < 0) {
perror("epoll_ctl");
return;
}

// 事件循环
struct epoll_event events[MAX_EVENTS];
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); // 阻塞等待
if (nfds < 0) {
if (errno == EINTR) continue; // 被信号中断,继续
perror("epoll_wait");
break;
}

for (int i = 0; i < nfds; i++) {
if (events[i].events & EPOLLIN) {
// 可读
int fd = events[i].data.fd;
// 处理读事件
} else if (events[i].events & (EPOLLERR | EPOLLHUP)) {
// 错误或挂断 → 移除监听
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
}
}
}
close(epfd);

epoll 的两种触发模式:

  • 水平触发(Level Triggered):只要 fd 有可读数据,每次 epoll_wait 都会返回该事件。类似 poll,编程简单。
  • 边缘触发(Edge Triggered):仅在 fd 状态从不可读变为可读时通知一次。需要循环 read 直到返回 EAGAIN,编程复杂但性能更好(减少重复通知)。

Android 的 android::Looper(Native 层)在 system/core/libutils/Looper.cpp 中,使用 epoll 监控文件描述符事件,同时支持定时器(通过 timerfd_create + epoll 实现)。

六、Unix Domain Socket

Unix Domain Socket 是同一台设备上进程间通信(IPC)的高效方式。相比 TCP/IP,它不需要经过网络协议栈,性能更高(延迟更低,吞吐更大)。

#include <sys/socket.h>
#include <sys/un.h>

// 服务端
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, "/data/local/tmp/my_socket", sizeof(addr.sun_path) - 1);
unlink(addr.sun_path); // 删除可能残留的 socket 文件

bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
listen(server_fd, 5);

int client_fd = accept(server_fd, NULL, NULL);

char buffer[1024];
ssize_t n = recv(client_fd, buffer, sizeof(buffer), 0);
// 处理请求
send(client_fd, "OK", 2, 0);

close(client_fd);
close(server_fd);
unlink(addr.sun_path);

Android 系统服务的 IPC 通常使用 Binder,但在一些性能敏感的场景(如 SurfaceFlinger 与 App 的 BufferQueue 通信、mediaserver 的音频数据传输)会使用 Unix Domain Socket 或匿名共享内存(ashmem)。

Android 的 installd 守护进程(包管理器后台服务)通过 Unix Domain Socket 接收 pm 命令:

installd → /dev/socket/installd (Unix Domain Socket)
→ install / uninstall / dexopt 等操作

七、匿名共享内存(ashmem)

Android 特有的 ashmem 驱动提供了进程间的匿名共享内存:

#include <sys/mman.h>
#include <linux/ashmem.h>
#include <sys/ioctl.h>

// 创建匿名共享内存
int fd = open("/dev/ashmem", O_RDWR);
if (fd < 0) {
perror("open /dev/ashmem");
return;
}

// 设置名称(用于调试)和大小
ioctl(fd, ASHMEM_SET_NAME, "MySharedBuffer");
ioctl(fd, ASHMEM_SET_SIZE, 4096);

// 映射到进程空间
void *shared_mem = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 可以将 fd 通过 Binder 传递给其他进程
// 其他进程使用同一 fd 或 dup 后的 fd 做 mmap

// 清理
munmap(shared_mem, 4096);
close(fd);

Android 的 MemoryFile(Java API)和 IMemory(Binder 接口)底层都是对 ashmem 的封装。ashmem 的特性是:内核通过引用计数跟踪共享内存的引用者,当所有引用者释放后自动回收内存。

八、CMake 构建配置

现代 Android NDK 项目使用 CMake 作为构建系统:

# CMakeLists.txt
cmake_minimum_required(VERSION 3.18.1)
project("nativeutils")

# 查找 NDK 提供的库
find_library(log-lib log) # liblog.so (__android_log_print)
find_library(z-lib z) # libz.so (压缩)

# 构建动态库
add_library(nativeutils SHARED
src/main/cpp/jni_main.cpp
src/main/cpp/crypto/aes_encrypt.cpp
src/main/cpp/ipc/socket_server.cpp
src/main/cpp/ipc/socket_client.cpp
)

# 包含头文件目录
target_include_directories(nativeutils PRIVATE
src/main/cpp/include
${CMAKE_CURRENT_SOURCE_DIR}/third_party/openssl/include
)

# 链接库
target_link_libraries(nativeutils
${log-lib}
${z-lib}
android # libandroid.so (AAssetManager 等)
openssl # 第三方预编译库
)

# 编译选项
target_compile_options(nativeutils PRIVATE
-Wall
-Werror
-O2
-DANDROID
)

app 模块的 build.gradle 中配置:

android {
defaultConfig {
ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.18.1"
}
}
}

九、面试常问题目

Q1: mmap 和 read/write 的区别?什么场景下用 mmap?

read/write 需要将数据从内核缓冲区拷贝到用户空间(一次数据拷贝)。mmap 将文件映射到进程的虚拟地址空间,通过缺页中断按需加载数据,不需要显式的 read/write 调用。mmap 适合:(1) 大文件的随机访问(只需访问其中一部分);(2) 多个进程共享同一文件的数据(MAP_SHARED);(3) 零拷贝的数据传输(如 Binder)。read/write 适合小文件、顺序读写。Android 的 DEX/OAT 加载使用 mmap。

Q2: epoll 的水平触发和边缘触发有什么区别?

水平触发(LT):只要 fd 仍有未处理的数据,每次 epoll_wait 都会返回该 fd 的事件。编程简单,与 select/poll 行为一致。边缘触发(ET):只在 fd 状态变化时(如从不可读变为可读)通知一次。必须循环读取直到返回 EAGAIN,否则可能丢失事件。ET 模式的优点是可以避免重复通知,减少 epoll_wait 的调用次数,适合高并发场景。Android Native Looper 默认使用 LT 模式。

Q3: Zygote fork 为什么比直接创建进程快?

Zygote 进程在启动时预加载了 Framework 类、JNI 库、系统资源(主题、字体等)。fork 使用 Copy-on-Write 机制,子进程共享这些预加载的资源,不需要重新加载。直接创建进程(fork + exec)需要一个全新的内存空间初始化过程,要重新执行所有加载步骤。Zygote 模式下,应用启动只需 fork(复制页表,约几毫秒),然后在新进程中初始化自己的少量组件。

Q4: Unix Domain Socket 和 TCP/IP Socket 的区别?在 Android 中如何选择?

Unix Domain Socket 用于同一台设备上的进程间通信,不需要经过 IP 层和 TCP 协议栈,数据在内核中直接传输(不经过网络设备),延迟更低、吞吐更高。TCP Socket 用于网络通信,可跨设备。在 Android 中,同一设备上的 IPC 首选 Binder(有权限管理、引用计数等高级特性),但当需要流式数据传输(如音频流)或需要支持非 Java 进程时,Unix Domain Socket 是合适的补充方案。


参考源码路径:

  • Android Looper(Native):system/core/libutils/Looper.cpp
  • Zygote 进程:frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
  • Zygote fork 流程:frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
  • Binder mmap:frameworks/native/libs/binder/ProcessState.cpp
  • debuggerd:system/core/debuggerd/
  • installd:frameworks/native/cmds/installd/
  • ashmem 驱动:kernel/common/drivers/staging/android/ashmem.c
  • Bionic epoll:bionic/libc/bionic/epoll_create.cpp
打赏
  • 微信
  • 支付宝

评论